﻿using System;
using System.Linq;
using System.Linq.Expressions;

namespace Hvam.SharePoint.Search.Language.Parsers
{
    internal class KeywordQueryParser : StringParser
    {
        internal static readonly Parser<string> PropertyName =
            from letters in Letter.Or(Digit).AtLeastOnce()
            select new string(letters.ToArray());

        internal static readonly Parser<ConstantExpression> Property =
            from name in PropertyName
            select Language.KeywordQueryExpression.Property(name);

        internal static readonly Parser<ExpressionType> EqualPropertyOperator =
            from op in Char('=').Once()
            select ExpressionType.Equal;

        internal static readonly Parser<ExpressionType> GreaterThanPropertyOperator =
            from op in Char('>').Once()
            select ExpressionType.GreaterThan;

        internal static readonly Parser<ExpressionType> LessThanPropertyOperator =
            from op in Char('<').Once()
            select ExpressionType.LessThan;

        internal static readonly Parser<ExpressionType> LessThanOrEqualPropertyOperator =
            from op in String("<=").Once()
            select ExpressionType.LessThanOrEqual;

        internal static readonly Parser<ExpressionType> GreaterThanOrEqualPropertyOperator =
            from op in String(">=").Once()
            select ExpressionType.GreaterThanOrEqual;

        internal static readonly Parser<ExpressionType> NotEqualPropertyOperator =
            from op in String("<>").Once()
            select ExpressionType.NotEqual;

        internal static readonly Parser<ExpressionType> PropertyOperator =
            from op in 
                EqualPropertyOperator.
                Else(GreaterThanOrEqualPropertyOperator).
                Else(LessThanOrEqualPropertyOperator).
                Else(NotEqualPropertyOperator).
                Else(GreaterThanPropertyOperator).
                Else(LessThanPropertyOperator)
            select op;

        internal static readonly Parser<ConstantExpression> BasicPropertyValue =
            from letters in 
                Letter.
                    Or(Digit).
                    Or(Char(':')).
                    Or(Char('/')).
                    Or(Char('.')).
                    Or(Char('-')).
                    Or(Char('#')).
                    Or(Char('_')).
                    AtLeastOnce()
            select Expression.Constant(new string(letters.ToArray()));

        internal static readonly Parser<ConstantExpression> QuotedPropertyValue =
            from q1 in Char('"').Once()
            from letters in 
                Letter.
                    Or(Digit).
                    Or(Char(':')).
                    Or(Char('/')).
                    Or(Char('.')).
                    Or(Char('-')).
                    Or(Char('#')).
                    Or(Char('_')).
                    Or(Char(' ')).
                    AtLeastOnce()
            from q2 in Char('"').Once()
            select Expression.Constant(new string(letters.ToArray()));

        internal static readonly Parser<ConstantExpression> PropertyValue =
            from value in QuotedPropertyValue.Else(BasicPropertyValue)
            select value;

        internal static readonly Parser<Expression> BasicPropertyRestriction =
            from left in Property
            from binaryType in PropertyOperator
            from right in PropertyValue
            select Expression.MakeBinary(binaryType, left, right) as Expression;

        internal static readonly Parser<Expression> ContainsPropertyRestriction =
            from left in Property
            from contains in Char(':').Once()
            from right in PropertyValue
            select Language.KeywordQueryExpression.Contains(left, right);

        internal static readonly Parser<Expression> PropertyRestriction =
            from restriction in 
                BasicPropertyRestriction.
                Else(ContainsPropertyRestriction)
            select restriction;

        internal static readonly Parser<Expression> ParenthesisExpression =
            from left in Char('(')
            from expression in Ref(() => KeywordQueryExpression)
            from right in Char(')')
            select expression;

        // TODO: left recursion
        internal static readonly Parser<Expression> AndExpression =
            from left in PropertyRestriction
            from leftWhitespace in WhiteSpace.AtLeastOnce()
            from and in String("AND").Once()
            from rightWhitespace in WhiteSpace.AtLeastOnce()
            from right in Ref(() => KeywordQueryExpression)
            select Expression.And(left, right) as Expression;

        internal static readonly Parser<Expression> OrExpression =
            from left in PropertyRestriction
            from leftWhitespace in WhiteSpace.AtLeastOnce()
            from or in String("OR").Once()
            from rightWhitespace in WhiteSpace.AtLeastOnce()
            from right in Ref(() => KeywordQueryExpression)
            select Expression.Or(left, right) as Expression;

        internal static readonly Parser<Expression> ImplicitExpression =
            from left in PropertyRestriction
            from whitespace in WhiteSpace.AtLeastOnce()
            from right in Ref(() => KeywordQueryExpression)
            select Expression.And(left, right) as Expression;

        internal static readonly Parser<Expression> KeywordQueryExpression =
            from expression in
                ParenthesisExpression.
                Else(AndExpression).
                Else(OrExpression).
                Else(ImplicitExpression).
                Else(PropertyRestriction)
            select expression;

        internal static readonly Parser<Expression> KeywordQueryExpressionLine =
            from expression in KeywordQueryExpression
            from eol in End<BinaryExpression>()
            select expression;

        public static Parser<T> Ref<T>(Func<Parser<T>> reference)
        {
            if (reference == null)
            {
                throw new ArgumentNullException("reference");
            }

            Parser<T> p = null;

            return
                i =>
                    {
                        if (p == null)
                        {
                            p = reference();
                        }


                        var result = p(i);

                        return result;
                    };
        }

        public static readonly Func<string, Expression> Parse =
            input => KeywordQueryExpressionLine.Parse(input);
    }
}
